vectorbt量化策略开发与回测实战(布林线策略)¶
量化小组学习1-杨欣琪
本教程将带你系统学习如何用vectorbt实现布林线量化策略,包括数据获取与处理、布林线信号生成、回测与绩效分析、参数优化等。
vectorbt 是一个基于 Python 的高性能量化回测与分析库,专为策略研究、参数优化和大规模回测设计。它支持灵活的数据处理、指标计算、信号生成、组合回测与可视化,能够帮助量化研究者和交易员高效地开发和评估各类金融策略。vectorbt 兼容 pandas 和 NumPy,易于与主流数据科学工具集成,适合快速原型开发和批量参数扫描。
我也调研了其他回测库比较轻量库例如 Backtesting 发现回测数据量大了就非常缓慢(应该是没有用NumPy写),总体使用不如vectorbt
In [80]:
import os
import pandas as pd
import plotly.io as pio
pio.renderers.default = 'notebook'
csv_path = '/home/xqyang/code/quant/TS/btcusd_1-min_data.csv' # 请替换为你的数据路径
assert os.path.exists(csv_path), f"文件不存在: {csv_path}"
df = pd.read_csv(csv_path)
df['Datetime'] = pd.to_datetime(df['Timestamp'], unit='s')
df = df.set_index('Datetime')
df = df.sort_index() # 保证索引单调递增,便于切片
# 选择时间范围:2024年1月1日-2024年6月30日(用loc避免KeyError)
df = df.loc[(df.index >= '2024-03-01') & (df.index <= '2024-03-15')]
# 选择K线粒度
TIMEFRAME = '5min' # 可选: '1min', '5min', '1H', '1D'等
ohlcv = df[['Open', 'High', 'Low', 'Close', 'Volume']].resample(TIMEFRAME).agg({
'Open': 'first',
'High': 'max',
'Low': 'min',
'Close': 'last',
'Volume': 'sum'
}).dropna()
close = ohlcv['Close']
ohlcv.tail()
Out[80]:
| Open | High | Low | Close | Volume | |
|---|---|---|---|---|---|
| Datetime | |||||
| 2024-03-14 23:40:00 | 68823.0 | 69230.0 | 68555.0 | 68922.0 | 98.832549 |
| 2024-03-14 23:45:00 | 68905.0 | 69487.0 | 68864.0 | 69336.0 | 45.376130 |
| 2024-03-14 23:50:00 | 69339.0 | 69800.0 | 69274.0 | 69701.0 | 80.362212 |
| 2024-03-14 23:55:00 | 69675.0 | 69709.0 | 69177.0 | 69339.0 | 122.128794 |
| 2024-03-15 00:00:00 | 69284.0 | 69586.0 | 69251.0 | 69535.0 | 6.501516 |
布林线策略设计逻辑说明¶
布林线(Bollinger Bands)是一种基于统计学原理的技术分析工具,主要用于衡量价格的波动性和判断超买超卖状态。其核心思想如下:
- 中轨:通常为N日移动平均线,反映价格的趋势。
- 上轨/下轨:分别为中轨加减K倍标准差,动态反映价格的波动区间。
策略逻辑设计¶
趋势突破信号
- 当价格上穿上轨,说明市场波动性增强且价格强势突破历史波动区间,视为多头信号(做多开仓)。
- 当价格下穿下轨,说明市场波动性增强且价格弱势跌破历史波动区间,视为空头信号(做空开仓)。
趋势反转/止盈信号
- 多头持仓时,若价格回落下穿中轨,说明上涨动能减弱,趋势可能反转,选择平多。
- 空头持仓时,若价格反弹上穿中轨,说明下跌动能减弱,趋势可能反转,选择平空。
设计原因¶
- 动态适应市场波动:布林线宽度随市场波动自动调整,能适应不同市场环境。
- 捕捉趋势行情:突破布林带往往伴随趋势行情启动,策略能及时跟随。
- 风险控制:以中轨作为止盈/止损线,有助于锁定利润或及时止损,避免趋势反转带来的损失。
综上,布林线策略兼具趋势跟踪和风险控制能力,适合震荡和趋势行情下的量化交易。
In [81]:
import vectorbt as vbt
import numpy as np
# 布林线参数
n = 200 # 最近200根5分钟K线的数据来计算均线和标准差 200 × 5 = 1000 分钟(约16.7小时)
m = 3 # m倍标准差
# 计算布林线
median = close.rolling(n, min_periods=1).mean()
std = close.rolling(n, min_periods=1).std(ddof=0)
upper = median + m * std
lower = median - m * std
# 做多信号:收盘价上穿上轨
entries_long = (close > upper) & (close.shift(1) <= upper.shift(1))
# 做多平仓:收盘价下穿中轨
exits_long = (close < median) & (close.shift(1) >= median.shift(1))
# 做空信号:收盘价下穿下轨
entries_short = (close < lower) & (close.shift(1) >= lower.shift(1))
# 做空平仓:收盘价上穿中轨
exits_short = (close > median) & (close.shift(1) <= median.shift(1))
# 合并信号
entries = entries_long | entries_short
exits = exits_long | exits_short
# 信号DataFrame展示
signal_df = pd.DataFrame({
'close': close,
'median': median,
'upper': upper,
'lower': lower,
'long_entry': entries_long,
'long_exit': exits_long,
'short_entry': entries_short,
'short_exit': exits_short
})
signal_df.tail()
Out[81]:
| close | median | upper | lower | long_entry | long_exit | short_entry | short_exit | |
|---|---|---|---|---|---|---|---|---|
| Datetime | ||||||||
| 2024-03-14 23:40:00 | 68922.0 | 72341.800 | 75957.139345 | 68726.460655 | False | False | False | False |
| 2024-03-14 23:45:00 | 69336.0 | 72322.665 | 75989.217077 | 68656.112923 | False | False | False | False |
| 2024-03-14 23:50:00 | 69701.0 | 72305.035 | 76008.181707 | 68601.888293 | False | False | False | False |
| 2024-03-14 23:55:00 | 69339.0 | 72285.690 | 76036.572333 | 68534.807667 | False | False | False | False |
| 2024-03-15 00:00:00 | 69535.0 | 72266.960 | 76056.665182 | 68477.254818 | False | False | False | False |
3. 回测与绩效分析(Backtesting)¶
本节将利用 vectorbt 的 Portfolio.from_signals 方法进行回测,并分析策略表现。
In [82]:
# 构建并回测布林线策略
portfolio = vbt.Portfolio.from_signals(
close,
entries=entries,
exits=exits,
short_entries=entries_short,
short_exits=exits_short,
init_cash=100000,
fees=0.001,
slippage=0.001,
direction='both'
)
# 查看全部绩效指标
stats = portfolio.stats()
# 英文到中文的映射
stats_cn_map = {
'Start': '开始时间',
'End': '结束时间',
'Period': '回测周期',
'Start Value': '初始资金',
'End Value': '结束资金',
'Total Return [%]': '总收益率 [%]',
'Benchmark Return [%]': '基准收益率 [%]',
'Max Gross Exposure [%]': '最大总风险敞口 [%]',
'Total Fees Paid': '总手续费',
'Max Drawdown [%]': '最大回撤 [%]',
'Max Drawdown Duration': '最大回撤持续时间',
'Total Trades': '总交易次数',
'Total Closed Trades': '已平仓交易数',
'Total Open Trades': '未平仓交易数',
'Open Trade PnL': '未平仓盈亏',
'Win Rate [%]': '胜率 [%]',
'Best Trade [%]': '最佳交易 [%]',
'Worst Trade [%]': '最差交易 [%]',
'Avg Winning Trade [%]': '平均盈利交易 [%]',
'Avg Losing Trade [%]': '平均亏损交易 [%]',
'Avg Winning Trade Duration': '平均盈利持仓时间',
'Avg Losing Trade Duration': '平均亏损持仓时间',
'Profit Factor': '盈利因子',
'Expectancy': '期望收益',
'Sharpe Ratio': '夏普比率',
'Calmar Ratio': '卡玛比率',
'Omega Ratio': '欧米伽比率',
'Sortino Ratio': '索提诺比率'
}
stats.index = [stats_cn_map.get(i, i) for i in stats.index]
display(stats)
# 查看部分交易明细
portfolio.trades.records_readable.tail()
/tmp/ipykernel_900975/1969587262.py:2: UserWarning: direction has no effect if short_entries and short_exits are set
开始时间 2024-03-01 00:00:00 结束时间 2024-03-15 00:00:00 回测周期 14 days 00:05:00 初始资金 100000.0 结束资金 99783.430591 总收益率 [%] -0.216569 基准收益率 [%] 14.585393 最大总风险敞口 [%] 100.0 总手续费 1195.279918 最大回撤 [%] 7.739755 最大回撤持续时间 9 days 17:10:00 总交易次数 6 已平仓交易数 6 未平仓交易数 0 未平仓盈亏 0.0 胜率 [%] 50.0 最佳交易 [%] 2.036248 最差交易 [%] -2.241125 平均盈利交易 [%] 1.308168 平均亏损交易 [%] -1.355226 平均盈利持仓时间 0 days 17:05:00 平均亏损持仓时间 0 days 07:41:40 盈利因子 0.947118 期望收益 -36.094902 夏普比率 0.02886 卡玛比率 -0.709879 欧米伽比率 1.000583 索提诺比率 0.036011 Name: Close, dtype: object
Out[82]:
| Exit Trade Id | Column | Size | Entry Timestamp | Avg Entry Price | Entry Fees | Exit Timestamp | Avg Exit Price | Exit Fees | PnL | Return | Direction | Status | Position Id | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 1 | Close | 1.538221 | 2024-03-04 13:05:00 | 64816.752 | 99.702497 | 2024-03-05 10:00:00 | 66267.666 | 101.934324 | 2030.189747 | 0.020362 | Long | Closed | 1 |
| 2 | 2 | Close | 1.496041 | 2024-03-07 19:35:00 | 67999.932 | 101.730659 | 2024-03-08 04:15:00 | 67028.904 | 100.277962 | -1654.705930 | -0.016266 | Long | Closed | 2 |
| 3 | 3 | Close | 1.470259 | 2024-03-08 19:05:00 | 68068.000 | 100.077606 | 2024-03-08 21:00:00 | 66677.256 | 98.032852 | -2242.864676 | -0.022411 | Long | Closed | 3 |
| 4 | 4 | Close | 1.421476 | 2024-03-10 06:00:00 | 68827.759 | 97.836982 | 2024-03-10 18:40:00 | 69011.919 | 98.098761 | 65.843209 | 0.000673 | Long | Closed | 4 |
| 5 | 5 | Close | 1.386596 | 2024-03-11 11:10:00 | 70606.536 | 97.902759 | 2024-03-12 04:50:00 | 72034.893 | 99.883314 | 1782.768414 | 0.018210 | Long | Closed | 5 |
In [83]:
# 可视化K线、布林线和买卖信号
import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(go.Candlestick(
x=ohlcv.index,
open=ohlcv['Open'],
high=ohlcv['High'],
low=ohlcv['Low'],
close=ohlcv['Close'],
name='K线'
))
fig.add_trace(go.Scatter(x=close.index, y=median, line=dict(color='blue'), name='布林中轨'))
fig.add_trace(go.Scatter(x=close.index, y=upper, line=dict(color='green'), name='布林上轨'))
fig.add_trace(go.Scatter(x=close.index, y=lower, line=dict(color='red'), name='布林下轨'))
# 标记做多/做空信号
fig.add_trace(go.Scatter(x=close.index[entries_long], y=close[entries_long], mode='markers', marker=dict(color='orange', symbol='triangle-up', size=10), name='做多开仓'))
fig.add_trace(go.Scatter(x=close.index[exits_long], y=close[exits_long], mode='markers', marker=dict(color='purple', symbol='circle', size=8), name='做多平仓'))
fig.add_trace(go.Scatter(x=close.index[entries_short], y=close[entries_short], mode='markers', marker=dict(color='cyan', symbol='triangle-down', size=10), name='做空开仓'))
fig.add_trace(go.Scatter(x=close.index[exits_short], y=close[exits_short], mode='markers', marker=dict(color='magenta', symbol='circle', size=8), name='做空平仓'))
fig.update_layout(title='布林线策略K线图', xaxis_title='时间', yaxis_title='价格')
fig.write_html("bollinger_signal.html")
fig
In [84]:
fig_value = go.Figure()
fig_value.add_trace(go.Scatter(
x=portfolio.value().index,
y=portfolio.value().values,
mode='lines',
name='资金曲线'
))
fig_value.update_layout(
title='Portfolio Value',
xaxis_title='时间',
yaxis_title='账户价值',
template='plotly_white'
)
fig_value.write_html("portfolio_value_bollinger.html")
fig_value
5. 参数优化(Optimization)¶
通过参数扫描,寻找最优布林线窗口和倍数组合。
In [85]:
# 布林线参数优化示例
import numpy as np
n_range = np.arange(100, 501, 20) # 窗口长度
m_range = np.arange(2.0, 5.1, 0.2) # 倍数
results = np.zeros((len(n_range), len(m_range)))
for i, n_val in enumerate(n_range):
for j, m_val in enumerate(m_range):
med = close.rolling(int(n_val), min_periods=1).mean()
st = close.rolling(int(n_val), min_periods=1).std(ddof=0)
up = med + m_val * st
low = med - m_val * st
ent_long = (close > up) & (close.shift(1) <= up.shift(1))
ext_long = (close < med) & (close.shift(1) >= med.shift(1))
ent_short = (close < low) & (close.shift(1) >= low.shift(1))
ext_short = (close > med) & (close.shift(1) <= med.shift(1))
ent = ent_long | ent_short
ext = ext_long | ext_short
pf = vbt.Portfolio.from_signals(
close,
entries=ent,
exits=ext,
short_entries=ent_short,
short_exits=ext_short,
init_cash=100000,
fees=0.001,
slippage=0.001,
direction='both'
)
results[i, j] = pf.total_return()
import plotly.graph_objects as go
fig = go.Figure(data=go.Surface(
z=results,
x=n_range,
y=m_range,
colorscale='Viridis'
))
fig.update_layout(
title='布林线参数优化三维收益图',
scene=dict(
xaxis_title='n(窗口)',
yaxis_title='m(倍数)',
zaxis_title='Total Return'
)
)
fig.write_html("bollinger_optimization_surface.html")
fig
/tmp/ipykernel_900975/3479930363.py:20: UserWarning: direction has no effect if short_entries and short_exits are set
- 从上图我们可以看到在寻找合适的参数的情况下,我们也能得到4%的收益,所以量化和机器学习一样需要很多调参工作
- 和学习一样,有的参数可能会存在局部最优和全局最优的情况,在图上我们可以看山峰的情况